Linux文件预读(三)

概述

前面两篇关于文件系统的预读我们通过实例阐述了应用程序的顺序读而触发操作系统对文件大小预取情况,本篇博客我们主要描述预读如何解决交织读的问题。所谓的交织读指的是多线程(进程)读同一个打开的文件描述符,单个线程的顺序读在操作系统看来可能会变成随机读。同样我们还是结合实例来分析。

情境3:交织读

事例代码

{
    ......
    f = open("file", ......)
    pthread_create(read_file_1, f, ...)
    pthread_create(read_file_2, f, ...)
    ......
}
read_file_1(f)
{
    lseek(f, 0, SEEK_SET);
    read(f, ..., 2 * 4096);
    read(f, ..., 4 * 4096)
    read(f, ..., 16 * 4096)
}

read_file_2(f)
{
    lseek(f, 128 * 4096, SEEK_SET);
    read(f, ..., 2 * 4096);
    read(f, ..., 4 * 4096)
    read(f, ..., 16 * 4096)
}

事例代码中创建了两个线程同时读文件file,每个线程均是顺序读,让我们看看操作系统的预读是如何处理这种情况的。因为多线程的执行顺序可能是多种多样的,我们只列举一种执行流并解释,线程1 read 1,线程2 read 1,线程2 read 2,线程1 read 2,线程1 read 3,线程2 read3。

线程1 Read 1

线程1读文件的前两个页面,由于尚未缓存命中,因此会触发文件系统的一次同步预读,确定预读窗口为

(ra->start, ra->size, ra->async_size) = (0, 4, 2)

形成的预读窗口如下:

Linux文件预读(三)

线程2 Read 1

线程2读文件的128和129两个页面,由于这两个页面也尚未缓存在page cache中,也必须启动一次同步预读,这里会更改上面的预读窗口为(128, 4, 2),更新后的预读窗口如下:

Linux文件预读(三)

线程2 Read 2

由于本次读和上次读是顺序读,且本次访问的4个页面有两个缓存命中,但由于访问了PAGE 130,而该页面又被打上了异步预读标记,因此在访问页面130的时候会触发一次异步预读,更新预读窗口为(132, 8, 8),如下:

Linux文件预读(三)

由于本次会访问4个页面,因此PAGE 132也会被访问,从而又触发一次异步预读,更新预读窗口为(140, 16, 16),最终形成的预读窗口如下:

Linux文件预读(三)

线程2两次读 read1 和read 2以后形成的page cache状态如下所示:

Linux文件预读(三)

线程1 Read 2

接下来线程1进行第二次读,范围是PAGE 2 ~ PAGE 5,由于线程1 read 1将PAGE 2 和PAGE 3已经预读进page cache,因此可直接命中,但在访问PAGE 2的时候会触发一次异步预读,所以这里会更新预读窗口,但很不幸,预读窗口保存的是线程2的预读状态,因此本次访问和之前的预读窗口并不连续,因此我们必须想办法来恢复线程1的之前的预读状态,会触发下面的执行逻辑:

if (hit_readahead_marker) {
        pgoff_t start;

        rcu_read_lock();
        //计算本次应该从哪个页面开始读
        // 计算的方法是:从上次的offset开始查找,找到第一个没有缓存在page cache 的页面
        start = radix_tree_next_hole(&mapping->page_tree, offset+1,max);
        rcu_read_unlock();

        // 如果没有找到或者与本次读的偏移相差甚大,那么其实无需再读了
        if (!start || start - offset > max)
            return 0;

        ra->start = start;
        ra->size = start - offset;  /* old async_size */
        ra->size += req_size;
        ra->size = get_next_ra_size(ra, max);
        //既然是异步预读,那读出的所有页面均是提前读的,因此设置async_size = size
        ra->async_size = ra->size;
        goto readit;
    }

这里恢复线程1的预读窗口方法也比较简单:从本次预读的页面开始向后搜索,找到第一个没有缓存在page cache的页面,本例中是page4,然后以此为本次预读的起始页面号,并可以计算出上次的预读窗口大小(page 4 – page 2 = 2),根据这两个值便可确定本次预读窗口为(4, 8, 8)。
更新后的预读窗口如下图所示:

Linux文件预读(三)

在访问页面4时,会再次出发异步预读,更新预读窗口为(8, 8, 8),如下图所示:

Linux文件预读(三)

因此,线程1经过read 1 和read 2,形成的page cache状态如下:

Linux文件预读(三)

线程1 Read 3

线程1第三次读的页面是PAGE 6 ~ PAGE 13,全部在缓存命中,但在访问PAGE 8的时候会触发一次异步预读,更新预读窗口为(16, 16, 16)。

在线程1经历了三次读以后,page cache的状态如下图所示:

Linux文件预读(三)

线程2 Read 3

线程2第三次读页面是PAGE 134 ~ PAGE 141,这些全在缓存中命中,但是访问PAGE 140时会触发一次异步预读。更新预读窗口,但是很不幸,之前的预读窗口是线程1的,因此我们必须搜寻才能恢复线程2的预读窗口,搜寻过程之前已经描述,这里不再啰嗦,恢复出线程2的预读窗口为(156, 32,32)。因此,总的来看,由于线程2的三次读形成的page cache状态如下:

Linux文件预读(三)

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>